﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Services.Protocols;

using SsrsRenderStudio.ReportExecutionService2005;
using SsrsRenderStudio.Shared;
using SsrsRenderStudio.KnownTypesAndModels;

//http://msdn.microsoft.com/en-us/library/ms155397.aspx     // device infos
//http://msdn.microsoft.com/en-us/library/ms156281.aspx
//http://msdn.microsoft.com/en-us/library/ms155081.aspx     // report lifecycle

namespace SsrsRenderStudio.Renderer
{
    public class RenderingEngine
    {
        private static EnumSupportedRenderer DEFAULT_RENDER_FORMAT = EnumSupportedRenderer.IMAGE;

        ReportExecutionService _renderEngine;           // instance of REX2005
        private ExecutionInfo _executionInfo = null;    // information about currently loaded report
        private Extension _renderFormat = null;         // format to use when rendering
        private string _executionUrl = null;            // path of REX
        private string _reportPath = null;              // path of report
        private byte[] _reportBytes = null;             //  or definition of report
        private string _deviceInfoAddl = "";            // DeviceInfo
        private string _deviceInfoDpi = "";             // DeviceInfo
        private bool _isMultiPageRender = true;         // do we render page at a time or all pages at once

        private Guid _engineId = Guid.Empty;
        public Guid EngineId { get { return _engineId; } }

        private Exception _thrownException;
        public string ThrownException
        {
            get
            {
                if (_thrownException != null)
                    return _thrownException.Message;

                return string.Empty;
            }
        }

        /// <summary>
        /// Allows user to change the report URL/RDL or REX URL without recreating the report engine instance.
        /// </summary>
        public void ChangeContext(string reportPath = null, byte[] reportDefinition = null, string executionUrl = null)
        {
            bool isChanged = false;

            if (reportPath != null)
            {
                Logger.Log("60014",  reportPath);
                _reportPath = reportPath;
                _reportBytes = null;
                isChanged = true;
            }
            else if (reportDefinition != null)
            {
                Logger.Log("60015");
                _reportBytes = reportDefinition;
                _reportPath = null;
                isChanged = true;
            }

            if (executionUrl != null)
            {
                Logger.Log("60016", executionUrl);
                _executionUrl = executionUrl;
                _renderEngine.Url = executionUrl;
                isChanged = true;
            }

            if (isChanged)
            {
                if (_executionInfo != null)
                {
                    _renderEngine.ResetExecution();
                    _executionInfo = null;
                }
            }

            if (_renderFormat == null)
                SetRenderFormat(DEFAULT_RENDER_FORMAT);
        }

        /// <summary>
        /// Instantiates a rendering engine.
        /// </summary>
        /// <param name="reportPath">Path to the report.</param>
        /// <param name="executionUrl">Server used to execute the report.</param>
        public RenderingEngine(string reportPath, string executionUrl): this()
        {
            // WCF upgrade
            // http://geekswithblogs.net/stun/archive/2010/02/26/executing-reporting-services-web-service-from-asp-net-mvc-using-wcf-add-service-reference.aspx
            
            ChangeContext(reportPath, null, executionUrl);
        }

        public RenderingEngine()
        {
            _engineId = Guid.NewGuid();
            _renderEngine = new ReportExecutionService();
            _renderEngine.Credentials = System.Net.CredentialCache.DefaultCredentials;
                
            // can't set render format here because we don't know what server we're hitting yet.
        }

        public Extension[] AvailableServerRenderingFormats
        {
            get 
            { 
                return new Extension[2] 
                    { 
                        new Extension { ExtensionType = ExtensionTypeEnum.Render, Name = "PDF", LocalizedName = "PDF" },
                        new Extension { ExtensionType = ExtensionTypeEnum.Render, Name = "IMAGE", LocalizedName = "TIFF file" }
                    };

                    // turned off for performance reasons.
                    //_serverRenderingFormats = _renderEngine.ListRenderingExtensions();
            }
        }

        /// <summary>
        /// Returns a list of supported rendering formats by this library.
        /// </summary>
        public Extension[] AvailableRenderingFormats
        {
            get 
            {
                List<Extension> serverExtensions = AvailableServerRenderingFormats.ToList();
                Extension[] libraryExtensions = new Extension[0];

                foreach (EnumSupportedRenderer r in Enum.GetValues(typeof(EnumSupportedRenderer))) 
                {
                    Extension e = serverExtensions.Find(item => item.Name.ToUpper() == r.ToString().ToUpper());
                    if (e != null)
                    {
                        Array.Resize(ref libraryExtensions, libraryExtensions.Length + 1);
                        libraryExtensions[libraryExtensions.Length - 1] = e;
                    }
                }

                return libraryExtensions;    
            }
        }

        private bool setRenderFormat(Extension extension)
        {
            EnumSupportedRenderer format = EnumSupportedRenderer.NONE;
            bool result = false;

            if (extension.ExtensionType == ExtensionTypeEnum.Render)
            {
                _renderFormat = extension;
                result = IsRenderFormatSupported(extension, out format);
            }

            UpdateDeviceInfo(format);
            return result;
        }

        private bool setRenderFormat(string formatName)
        {
            Extension r = AvailableServerRenderingFormats.ToList().Find(item => item.Name.ToUpper() == formatName && item.ExtensionType == ExtensionTypeEnum.Render);
            if (r == null) return false;

            return (setRenderFormat(r));
        }

        public bool SetRenderFormat(EnumSupportedRenderer format, int dpiX = 0, int dpiY = 0)
        {
            switch (format)
            {
                case EnumSupportedRenderer.IMAGE:
                    setDisplayDpi(dpiX, dpiY);
                    break;
                case EnumSupportedRenderer.PDF:
                    setPrintDpi(dpiX, dpiY);
                    break;
            }

            if (_renderFormat == null || CurrentRenderFormatAsEnum != format)
                return setRenderFormat(format.ToString());

            return true;
        }

        /// <summary>
        /// Check the requested renderer against the supported renderers.
        /// </summary>
        private bool IsRenderFormatSupported(Extension e, out EnumSupportedRenderer format)
        {
            format = EnumSupportedRenderer.NONE;
            if (Enum.TryParse<EnumSupportedRenderer>(e.Name.ToUpper(), out format))
            {
                return true;
            }

            return false;
        }
        
        private void setDisplayDpi(int x, int y)
        {
            if (x == 0)
            {
                x = 203;
                int.TryParse(Properties.Resources.DEVICE_INFO_DPI_X_DEFAULT, out x);
            }

            if (y == 0)
            {
                y = 203;
                int.TryParse(Properties.Resources.DEVICE_INFO_DPI_Y_DEFAULT, out y);
            }

            _deviceInfoDpi = string.Format(Properties.Resources.DEVICE_INFO_DPI, x, y);
        }

        private void setPrintDpi(int x, int y)
        {
            if (x == 0)
            {
                x = 300;
                int.TryParse(Properties.Resources.DEVICE_INFO_PRINT_DPI_X_DEFAULT, out x);
            }

            if (y == 0)
            {
                y = 300;
                int.TryParse(Properties.Resources.DEVICE_INFO_PRINT_DPI_Y_DEFAULT, out y);
            }

            _deviceInfoDpi = string.Format(Properties.Resources.DEVICE_INFO_PRINT_DPI, x, y);
        }

        /// <summary>
        /// This method is called internally after the render format is set.
        /// This method builds additional DeviceInfo string as well as any format nuances.
        /// </summary>
        private void UpdateDeviceInfo(EnumSupportedRenderer format)
        {
            switch (format)
            {
                case EnumSupportedRenderer.IMAGE:
                    _isMultiPageRender = true;
                    _deviceInfoAddl = Properties.Resources.DEVICE_INFO_OUTPUT_FORMAT_EMF;
                    break;
                case EnumSupportedRenderer.PDF:
                    _isMultiPageRender = false;
                    _deviceInfoAddl = "";
                    break;
                default:
                    _deviceInfoAddl = "";
                    break;
            }
        }

        /// <summary>
        /// What is the current rendering format set to?
        /// </summary>
        public Extension CurrentRenderFormat
        {
            get { return _renderFormat; }
        }

        public EnumSupportedRenderer CurrentRenderFormatAsEnum
        {
            get
            {
                return (EnumSupportedRenderer)Enum.Parse(typeof(EnumSupportedRenderer), _renderFormat.Name);
            }
        }

        /// <summary>
        /// Get the page setting's from the renderer's ExecutionInfo.
        /// Report margins and page size.
        /// Report must be loaded for this to return.
        /// </summary>
        public PageSettings10 ReportPageSettings
        {
            get
            {
                if (_executionInfo == null) return null;

                PageSettings ps =  _executionInfo.ReportPageSettings;
                return new PageSettings10()
                    {
                        MarginTop = ps.Margins.Top,
                        MarginBottom = ps.Margins.Bottom,
                        MarginLeft = ps.Margins.Left,
                        MarginRight = ps.Margins.Right,
                        Height = ps.PaperSize.Height,
                        Width = ps.PaperSize.Width,
                        IsMetric = true
                    };

                //WARNING: TODO: here's hoping that RDL come in as Metric

            }
        }

        /// <summary>
        /// Get the required parameters.
        /// </summary>
        public ReportParameter[] ReportParameters
        {
            get
            {
                if (_executionInfo != null)
                    return _executionInfo.Parameters;

                return null;
            }
        }

        private ParameterValue[] ConvertHashToParameter(System.Collections.Hashtable hash)
        {
            if (hash == null)
                return null;

            ParameterValue[] parameters = new ParameterValue[hash.Count];

            int count = 0;
            foreach (System.Collections.DictionaryEntry h in hash)
            {
                parameters[count] = new ParameterValue();
                parameters[count].Name = (string)h.Key;
                parameters[count].Value = (string)h.Value;
                count++;
            }

            return parameters;
        }

        /// <summary>
        /// Loads, executes and renders the report.
        /// </summary>
        /// <param name="renderedReport">(out) Report buffer.</param>
        /// <param name="extension">(out) File extension produced by the buffer.</param>
        /// <param name="encoding">(out) Encoding type of the buffer.</param>
        /// <param name="parameters">(in) Query Parameters.  Supported Types: HashTable or ParameterValue</param>
        /// <returns></returns>
        public EnumServiceSideException RenderReport(out byte[][] renderedReport, out string extension, out string encoding, dynamic parameters = null) // ParameterValue[] parameters = null)
        {
            // set the method outs
            encoding = null;
            extension = null;
            renderedReport = null;
            EnumServiceSideException renderException = EnumServiceSideException.NONE;

            if ( ( _reportPath == null && _reportBytes == null ) || _executionUrl == null) 
                return EnumServiceSideException.RENDERER_ENGINE_CONTEXT_NOT_SETUP;

            int numberOfPages;
            Byte[][] pages = null;
            
            // render outs
            string mimeType;
            Warning[] warnings = null;
            string[] streamIDs = null;

            // render ins
            string deviceInfo = null;
            
            try
            {
                try
                {
                    // either load the URL or the RDL
                    if (_reportPath != null)
                    {
                        _executionInfo = _renderEngine.LoadReport(_reportPath, null);
                    }
                    else if (_reportBytes != null)
                    {
                        _executionInfo = _renderEngine.LoadReportDefinition2(_reportBytes, out warnings);
                    }

                }
                catch (SoapException ex)
                {
                    _thrownException = ex;
                    return EnumServiceSideException.RENDERER_LOAD_REPORT_FAILED_INVALID_REPORT;
                }
                catch (System.Net.WebException ex)
                {
                    _thrownException = ex;
                    return EnumServiceSideException.RENDERER_REX_DOWN;
                }
                catch (InvalidOperationException ex)
                {
                    _thrownException = ex;
                    return EnumServiceSideException.RENDERER_REX_MALFORMED;
                }
                catch (Exception ex)
                {
                    _thrownException = ex;
                    return EnumServiceSideException.RENDERER_LOAD_REPORT_FAILED;
                }

                if (parameters != null)
                {
                    if (parameters.GetType() == typeof(System.Collections.Hashtable))
                    {
                        _renderEngine.SetExecutionParameters(ConvertHashToParameter(parameters), Properties.Resources.REX_PARAMETER_LANGUAGE);
                    }
                    else if (parameters.GetType() == typeof(ParameterValue[]))
                    {
                        _renderEngine.SetExecutionParameters(parameters, Properties.Resources.REX_PARAMETER_LANGUAGE);
                    }
                }

                #region streamId approach
                //TODO doesn't work on multipage report
                /*
                // Renders the first page of the report and returns streamIDs for subsequent pages
                firstPage = _renderEngine.Render(_renderFormat.Name, deviceInfo, out extension, out encoding, out mimeType, out warnings, out streamIDs);
                // The total number of pages of the report is 1 + the streamIDs         
                numberOfPages = streamIDs.Length + 1;
                pages = new Byte[numberOfPages][];
                // The first page was already rendered
                pages[0] = firstPage;

                for (int pageIndex = 1; pageIndex < numberOfPages; pageIndex++)
                {
                    // Build device info based on start page
                    deviceInfo = String.Format(@"<DeviceInfo>" + _deviceInfoAddl + "<StartPage>{0}</StartPage></DeviceInfo>", pageIndex + 1);
                    pages[pageIndex] = _renderEngine.Render(_renderFormat.Name, deviceInfo, out extension, out encoding, out mimeType, out warnings, out streamIDs);
                }
                */
                #endregion

                pages = new Byte[0][];
                numberOfPages = 1;
                byte[] currentPageStream = new byte[1] {0x00}; // put a byte to get the loop started

                while (currentPageStream.Length > 0)
                {   
                    deviceInfo = String.Format(Properties.Resources.DEVICE_INFO_TEMPLATE, _deviceInfoAddl, _deviceInfoDpi, _isMultiPageRender ? numberOfPages : 0);
                    currentPageStream = _renderEngine.Render(_renderFormat.Name, deviceInfo, out extension, out encoding, out mimeType, out warnings, out streamIDs);

                    if (currentPageStream.Length == 0 && numberOfPages == 1)
                    {
                        renderException = EnumServiceSideException.RENDERER_ZERO_LENGTH_STREAM;
                        break;
                    }

                    //TODO check warnings

                    if (currentPageStream.Length > 0)
                    {
                        Array.Resize(ref pages, pages.Length + 1);
                        pages[pages.Length - 1] = currentPageStream;
                        numberOfPages++;

                        if (!_isMultiPageRender) currentPageStream = new byte[0];
                    }
                }

                numberOfPages--;
            }
            catch (SoapException ex)
            {
                _thrownException = ex;
                renderException = EnumServiceSideException.RENDERER_SOAP_ERROR;
            }
            catch (Exception ex)
            {
                _thrownException = ex;
                renderException = EnumServiceSideException.RENDERER_GENERIC_ERROR;
            }
            
            renderedReport = pages;

            return renderException;
        }

        public void ResetExecution()
        {
            _executionInfo = _renderEngine.ResetExecution();
        }
    }
}
